package libs.game.sokoban;

import libs.game.*;
import java.util.Collection;
import java.util.HashSet;

/**
 * 				This class represents a game plan of sokoban.
 *
 * @author		Jan Papousek
 * @version		2009-02-17
 */
public class SokobanGamePlan implements GamePlan {

    /**
     * 			The char which represents a field with box on empty place.
     */
    public static final char FIELD_BOX_ON_EMPTY = '$';
    /**
     * 			The char which represents a field with box on goal place.
     */
    public static final char FIELD_BOX_ON_GOAL = '*';
    /**
     * 			The char which represents an empty field.
     */
    public static final char FIELD_EMPTY = ' ';
    /**
     * 			The char which represents a goal field.
     */
    public static final char FIELD_GOAL = '.';
    /**
     * 			The char which represents an empty field where the person is.
     */
    public static final char FIELD_PERSON_ON_EMPTY = '@';
    /**
     * 			The char which represents a goal field where the person is.
     */
    public static final char FIELD_PERSON_ON_GOAL = '+';
    /**
     * 			The char which represents a wall field.
     */
    public static final char FIELD_WALL = '#';
    /**
     *          Move to the east.
     */
    public static final int MOVE_EAST = 1;
    /**
     *          Move to the north.
     */
    public static final int MOVE_NORTH = 2;
    /**
     *          Move to the south.
     */
    public static final int MOVE_SOUTH = 3;
    /**
     *          Move to the west.
     */
    public static final int MOVE_WEST = 4;
    /**
     *          Coordinates of boxes.
     */
    private Collection<Coordinates> boxes = new HashSet<Coordinates>();
    /**
     *          Coordinates of goals.
     */
    private Collection<Coordinates> goals = new HashSet<Coordinates>();
    /**
     *          Coordinates of the person.
     */
    private Coordinates person;
    /**
     * 			The game plan.
     */
    private char[][] plan;
    /**
     *          Number of box moves.
     */
    private int numOfBoxMoves = 0;

    /**
     * 			The constructor creates an empty plan with specific width and height.
     *
     * @param	plan		The game plan represented in array.
     * @param   person      Coordinates of the person.
     * @throws	NullPointerException if the plan or person is null.
     * @throws  IllegalArgumentException if the field with person coordinates doesn't contains a person,
     *          num of people is more than one or there is not the same num of boxes as goals.
     */
    public SokobanGamePlan(char[][] plan) {
        if (plan == null) {
            throw new NullPointerException("The game plan is null!");
        }
        if (plan.length == 0) {
            throw new IllegalArgumentException("The game plan has no rows");
        }
        if (plan[0].length == 0) {
            throw new IllegalArgumentException("The game plan has no columns");
        }
        Coordinates person = null;
        int x = 0;
        int y = 0;
        this.plan = new char[plan.length][plan[0].length];
        for (char[] row : plan) {
            for (char field : row) {
                switch (field) {
                    case SokobanGamePlan.FIELD_PERSON_ON_EMPTY:
                        if (person != null) {
                            throw new IllegalArgumentException("There are more than one person at the plan.");
                        }
                        person = new Coordinates(x, y);
                        break;
                    case SokobanGamePlan.FIELD_PERSON_ON_GOAL:
                        if (person != null) {
                            throw new IllegalArgumentException("There are more than one person at the plan.");
                        }
                        person = new Coordinates(x, y);
                        this.goals.add(new Coordinates(x, y));
                        break;
                    case SokobanGamePlan.FIELD_BOX_ON_EMPTY:
                        this.boxes.add(new Coordinates(x, y));
                        break;
                    case SokobanGamePlan.FIELD_BOX_ON_GOAL:
                        this.boxes.add(new Coordinates(x, y));
                        this.goals.add(new Coordinates(x, y));
                        break;
                    case SokobanGamePlan.FIELD_GOAL:
                        this.goals.add(new Coordinates(x, y));
                        break;
                    case SokobanGamePlan.FIELD_WALL:
                    case SokobanGamePlan.FIELD_EMPTY:
                        break;
                    default:
                        throw new IllegalArgumentException("There is an unsupported field type.");
                }
                this.plan[y][x] = plan[y][x];
                x++;
            }
            x = 0;
            y++;
        }
        if (this.boxes.size() != this.goals.size()) {
            throw new IllegalArgumentException("There is not the same number of boxes as goals.");
        }
        if (person == null) {
            throw new IllegalArgumentException("There is no person at the plan.");
        }
        this.person = person;
    }

    /**
     *              It changes a box position.
     * @param       from        Original field.
     * @param       to          Target field.
     * @return      TRUE if the change was successful, otherwise FALSE.
     */
    public boolean changeBox(Coordinates from, Coordinates to) {
        if (from == null) {
            throw new NullPointerException("Argument 'from' is null.");
        }
        if (to == null) {
            throw new NullPointerException("Argument 'to' is null.");
        }
        if (getField(to) != FIELD_EMPTY && getField(to) != FIELD_GOAL) {
            return false;
        }
        switch(getField(from)) {
            case FIELD_BOX_ON_EMPTY:
                plan[from.getY()][from.getX()] = FIELD_EMPTY;
                break;
            case FIELD_BOX_ON_GOAL:
                plan[from.getY()][from.getX()] = FIELD_GOAL;
                break;
            default:
                throw new IllegalArgumentException("There is no box on 'from' field.");
        }
        if (getField(to) == FIELD_EMPTY) {
            plan[to.getY()][to.getX()] = FIELD_BOX_ON_EMPTY;
        }
        else {
            plan[to.getY()][to.getX()] = FIELD_BOX_ON_GOAL;
        }
        boxes.remove(from);
        boxes.add(to);
        return true;
    }

    /**
     *              It changes a person position.
     * @param       personCoords    Target field.
     * @return      TRUE if the change was successful, otherwise FALSE.
     */
    public boolean changePerson(Coordinates personCoords) {
        if (personCoords == null) {
            throw new NullPointerException("Argument 'personCoords' is null.");
        }
        switch (this.getField(personCoords)) {
            case FIELD_EMPTY:
                plan[personCoords.getY()][personCoords.getX()] = FIELD_PERSON_ON_EMPTY;
                break;
            case FIELD_GOAL:
                plan[personCoords.getY()][personCoords.getX()] = FIELD_PERSON_ON_GOAL;
                break;
            default:
                return false;
        }
        if (getField(person) == FIELD_PERSON_ON_EMPTY) {
            plan[person.getY()][person.getX()] = FIELD_EMPTY;
        } else {
            plan[person.getY()][person.getX()] = FIELD_GOAL;
        }
        person = personCoords;
        return true;
    }

    @Override
    public int columns() {
        if (this.rows() > 0) {
            return this.plan[0].length;
        } else {
            return 0;
        }
    }

    @Override
    public char[][] getPlan() {
        return this.plan;
    }

    @Override
    public char getField(Coordinates coords) {
        if (!coords.isInRange(0, 0, columns() - 1, rows() - 1)) {
            throw new IllegalArgumentException("Coordination is out of the plan.");
        }
        return this.plan[coords.getY()][coords.getX()];
    }

    /**
     *          It returns coordinates of boxes.
     *
     * @return  Coordinates of the boxes.
     */
    public Collection<Coordinates> getBoxes() {
        return this.boxes;
    }

    /**
     *          It returns coordinates of goals.
     *
     * @return  Coordinates of the goals.
     */
    public Collection<Coordinates> getGoals() {
        return this.goals;
    }

    /**
     *          It returns coordinates of the person.
     * @return  Coordinates
     */
    public Coordinates getPersonCoords() {
        return this.person;
    }

    @Override
    public boolean move(int movementType) {

        int toRow = person.getY();          // Coordinate of the row.
        int toColumn = person.getX();       // Coordinate of the column.
        int boxToRow = person.getY();
        int boxToColumn = person.getX();

        // The first I detect the move type.
        switch (movementType) {
            default:
                throw new UnsupportedOperationException("This move is not supported.");
            case SokobanGamePlan.MOVE_EAST:
                boxToColumn += 2;
                toColumn++;
                break;
            case SokobanGamePlan.MOVE_NORTH:
                toRow--;
                boxToRow -= 2;
                break;
            case SokobanGamePlan.MOVE_SOUTH:
                toRow++;
                boxToRow += 2;
                break;
            case SokobanGamePlan.MOVE_WEST:
                toColumn--;
                boxToColumn -= 2;
                break;

        }

        // I check if the coordinates are out of the plan matrix.
        if ((toColumn < 0) || (toRow < 0) || (toColumn >= plan[0].length) || (toRow >= plan.length)) {
            return false;
        }

        // I find the type of the fields which are changed by the move.
        char toField = 'x';
        char toBoxField = 'x';
        switch (plan[toRow][toColumn]) {
            default:
                return false;
            case SokobanGamePlan.FIELD_GOAL:
                toField = SokobanGamePlan.FIELD_PERSON_ON_GOAL;
                break;
            case SokobanGamePlan.FIELD_EMPTY:
                toField = SokobanGamePlan.FIELD_PERSON_ON_EMPTY;
                break;
            case SokobanGamePlan.FIELD_BOX_ON_EMPTY:
                toField = SokobanGamePlan.FIELD_PERSON_ON_EMPTY;
            case SokobanGamePlan.FIELD_BOX_ON_GOAL:
                if (toField == 'x') {
                    toField = SokobanGamePlan.FIELD_PERSON_ON_GOAL;
                }
                if ((boxToColumn < 0) || (boxToRow < 0) || (boxToColumn >= plan[0].length) || (boxToRow >= plan.length)) {
                    return false;
                }
                switch (plan[boxToRow][boxToColumn]) {
                    default:
                        return false;
                    case SokobanGamePlan.FIELD_GOAL:
                        toBoxField = SokobanGamePlan.FIELD_BOX_ON_GOAL;
                        break;
                    case SokobanGamePlan.FIELD_EMPTY:
                        toBoxField = SokobanGamePlan.FIELD_BOX_ON_EMPTY;
                        break;
                }
        }

        // I change the field where the person moves from.
        if (this.plan[this.person.getY()][this.person.getX()] == SokobanGamePlan.FIELD_PERSON_ON_GOAL) {
            this.plan[this.person.getY()][this.person.getX()] = SokobanGamePlan.FIELD_GOAL;
        } else {
            this.plan[this.person.getY()][this.person.getX()] = SokobanGamePlan.FIELD_EMPTY;
        }

        // The person's move
        this.person = new Coordinates(toColumn, toRow);
        this.plan[this.person.getY()][this.person.getX()] = toField;

        // The box move.
        if (toBoxField != 'x') {
            this.boxes.remove(new Coordinates(toColumn, toRow));
            this.boxes.add(new Coordinates(boxToColumn, boxToRow));
            this.plan[boxToRow][boxToColumn] = toBoxField;
            numOfBoxMoves++;
        }
        return true;
    }

    /**
     *          It returns number of box moves.
     *
     * @return  Number of box moves.
     */
    @Override
    public int numberOfMoves() {
        return numOfBoxMoves;
    }

    @Override
    public int rows() {
        return this.plan.length;
    }

    @Override
    public String toString() {
        StringBuffer result = new StringBuffer();
        for (char[] row : this.plan) {
            for (char item : row) {
                result.append(item);
            }
            result.append('\n');
        }
        return result.toString();
    }

    @Override
    public boolean equals(Object o) {

        // THIS METHOD IS CORRECT.

        if (!(o instanceof SokobanGamePlan)) {
            return false;
        }
        SokobanGamePlan plan = (SokobanGamePlan) o;
        if (plan.rows() != this.rows() || plan.columns() != this.columns()) {
            return false;
        }
        if (!new PersonPath(plan).equals(new PersonPath(this))) {
            return false;
        }
        for (int row = 0; row < this.rows(); row++) {
            for (int column = 0; column < this.columns(); column++) {
                Coordinates current = new Coordinates(column, row);
                switch (plan.getField(current)) {
                    case SokobanGamePlan.FIELD_EMPTY:
                        if (this.getField(current) == SokobanGamePlan.FIELD_PERSON_ON_EMPTY) {
                            continue;
                        }
                    case SokobanGamePlan.FIELD_GOAL:
                        if (this.getField(current) == SokobanGamePlan.FIELD_PERSON_ON_GOAL) {
                            continue;
                        }
                    case SokobanGamePlan.FIELD_PERSON_ON_EMPTY:
                        if (this.getField(current) == SokobanGamePlan.FIELD_EMPTY) {
                            continue;
                        }
                    case SokobanGamePlan.FIELD_PERSON_ON_GOAL:
                        if (this.getField(current) == SokobanGamePlan.FIELD_GOAL) {
                            continue;
                        }
                    default:
                        if (plan.getField(current) != getField(current)) {
                            return false;
                        }
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 17 * hash + (this.boxes != null ? this.boxes.size() : 0);
        return hash;
    }
}
